import { task, src, dest, series, parallel } from 'gulp';
import * as path from 'path';
import * as fs from 'fs';

import { walkDirAndFilter, getBaseNameWithoutExt } from '../util';
import { srcPath, autoPath, docAppPath } from '../config';

import './doc-tasks/doc-task-api';
import './doc-tasks/doc-task-model';
import './doc-tasks/doc-task-color';

const transform = require('gulp-transform');
const rename = require('gulp-rename');
const flatten = require('gulp-flatten');
const htmlmin = require('gulp-htmlmin');
const markdown = require('gulp-markdown');
const hljs = require('highlight.js');
const dom = require('gulp-dom');

// Options for the html-minifier that minifies the generated HTML files.
const htmlMinifierOptions = {
  collapseWhitespace: true,
  removeComments: true,
  caseSensitive: true,
  removeAttributeQuotes: false,
};

// Our docs contain comments of the form `<!-- example(...) -->` which serve as placeholders where
// example code should be inserted. We replace these comments with divs that have a
// `fui-docs-example` attribute which can be used to locate the divs and initialize the example
// viewer.
const EXAMPLE_PATTERN = /<!--\W*example\(([^)]+)\)\W*-->/g;

// HTML tags in the markdown generated files that should receive a .docs-markdown-${tagName} class
// for styling purposes.
const MARKDOWN_TAGS_TO_CLASS_ALIAS = [
  'a',
  'h1',
  'h2',
  'h3',
  'h4',
  'h5',
  'li',
  'ol',
  'p',
  'table',
  'tbody',
  'td',
  'th',
  'tr',
  'ul',
  'pre',
  'code',
];

const markdownOptions = {
  // Add syntax highlight using highlight.js
  highlight: (code: string, language: string): string => {
    if (language) {
      // highlight.js expects "typescript" written out, while Github supports "ts".
      const lang = language.toLowerCase() === 'ts' ? 'typescript' : language;
      return hljs.highlight(lang, code).value;
    }

    return code;
  },
};

task('markdown-docs', () => {
  const renderer = {
    // Encode href for compatibility with Chinese id and id with spaces
    heading(text: string, level: number, raw: string) {
      const serializedText = raw
        .trim()
        // remove html tags
        .replace(/<[!\/a-z].*?>/ig, '');
      const encodedText = encodeURIComponent(serializedText);
      return `<h${level} id="${encodedText}">${text}</h${level}>`;
    },
  };
  markdown.marked.use({ renderer });

  return src(['README.md', 'CHANGELOG.md', 'guides/**/*.md'])
    .pipe(rename({ extname: '.html' }))
    .pipe(markdown(markdownOptions))
    .pipe(transform('utf8', transformMarkdownFiles))
    .pipe(dom(createTagNameAliaser('docs-markdown')))
    .pipe(dest('doc/assets/markdown'));
});

task('createGuides', (callback) => {
  const navs: string[] = [];

  walkDirAndFilter('./guides', '.md', (ele: any) => {
    navs.push(getBaseNameWithoutExt(ele.path));
  });

  fs.writeFileSync(
    path.join(autoPath, 'guides.ts'), `export default ${JSON.stringify(['README', ...navs, 'CHANGELOG'])}`,
  );
  callback();
});

task('createNav', (callback) => {
  const navs: string[] = [];

  walkDirAndFilter(path.join(srcPath, './fui'), '.md', (ele: any) => {
    navs.push(getBaseNameWithoutExt(ele.path));
  });

  walkDirAndFilter(path.join(docAppPath, '../guides/component-guides'), '.md', (ele: any) => {
    navs.push(getBaseNameWithoutExt(ele.path));
  });

  fs.writeFileSync(
    path.join(autoPath, 'navs.ts'), `export default ${JSON.stringify(navs)}`,
  );
  callback();
});

task('copy-examples', () => {
  // rename files to fit format: [filename]-[filetype].txt
  const renameFile = (filePath: any) => {
    const extension = filePath.extname.slice(1);
    filePath.basename = `${filePath.basename}-${extension}`;
    filePath.extname = '.txt';
  };

  return src('doc/fui-examples/**/*.+(html|css|ts)')
    .pipe(flatten())
    .pipe(rename(renameFile))
    .pipe(dest('doc/assets/examples'));
});

/** Generates html files from the markdown overviews and guides for fui. */
task('markdown-docs-fui', () => {
  const renderer = {
    // Extend the renderer for custom heading anchor rendering
    heading(text: string, level: number) {
      if (level === 1 || level === 2) {
        return `<h${level} id="${text}" fui-docs-example-title="${text}">${text}</h${level}>`;
      } else {
        const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
        return `
          <h${level} id="${escapedText}" class="docs-header-link">
            <span header-link="${escapedText}"></span>
            ${text}
          </h${level}>
        `;
      }
    },
  };
  markdown.marked.use({ renderer });

  return src(['src/fui/**/!(README).md', 'doc/guides/component-guides/**/*.md', 'doc/guides/*.md'])
    .pipe(rename({ prefix: 'fui-', extname: '.html' }))
    .pipe(markdown(markdownOptions))
    .pipe(transform('utf8', transformMarkdownFiles))
    .pipe(dom(createTagNameAliaser('docs-markdown')))
    .pipe(dest('doc/assets/markdown'));
});

task('minify-html-files', () => {
  return src('doc/assets/markdown/**/*.html')
    .pipe(htmlmin(htmlMinifierOptions))
    .pipe(dest('doc/assets/markdown'));
});

task('docs', series(
  'prework',
  'markdown-docs',
  parallel(
    'createGuides',
    'createNav',
    'genAPI',
    'genModel',
    'genColors',
    'copy-examples',
    'markdown-docs-fui',
    'build-examples-module',
  ),
  'minify-html-files',
));


task('docs-inc', series(
  'prework',
  'markdown-docs',
  parallel(
    'createGuides',
    'createNav',
    'genAPI',
    'genModel',
    'genColors',
    'copy-examples',
    'markdown-docs-fui',
    'build-examples-module',
  ),
  'minify-html-files',
));

/** Updates the markdown file's content to work inside of the docs app. */
function transformMarkdownFiles(buffer: Buffer, file: any): string {
  let content = buffer.toString('utf-8');

  // Replace <!-- example(..) --> comments with HTML elements.
  content = content.replace(EXAMPLE_PATTERN, (_match: string, name: string) =>
    `<div class="docs-example-item"><div fui-docs-example="${name}"></div></div>`,
  );

  // Finally, wrap the entire generated in a doc in a div with a specific class.
  return `<div class="docs-markdown docs-examples">
    <div class="docs-container">
      ${content}
      <div class="docs-api"></div>
      <div class="docs-model"></div>
    </div>
  </div>`;
}

/**
 * Returns a function to be called with an HTML document as its context that aliases HTML tags by
 * adding a class consisting of a prefix + the tag name.
 * @param classPrefix The prefix to use for the alias class.
 */
function createTagNameAliaser(classPrefix: string) {
  return function () {
    MARKDOWN_TAGS_TO_CLASS_ALIAS.forEach(tag => {
      for (const el of this.querySelectorAll(tag)) {
        el.classList.add(`${classPrefix}-${tag}`);
      }
    });

    return this;
  };
}
